/**
 * 常用工具 cocos 2.4
 * 2022-12-26
 * @author xaioyi
 */
export default class Utils {
    /**浏览器中，获取外层提供的数据，通过约定的字段 */
    public static getDataFromExternal(field: string, defaultValue: any): any {
        let top = window.parent as any;
        if (typeof top[field] != 'undefined') {
            return top[field];
        }
        return defaultValue;
    }

    /**
     * 加载资源Promise化
     * @param path 资源相对resources的路径
     * @returns 
     */
    public static loadRes(path: string): Promise<any> {
        return new Promise<any>((resolve, reject) => {
            cc.resources.load(path, (error, data) => {
                if (error) {
                    cc.warn(error);
                    reject(error);
                } else {
                    resolve(data);
                }
            })
        })
    }

    /**
     * 加载远程资源Promise化
     * @param remoteUrl 远程资源URL地址
     * @returns 
     */
    public static loadRemoteRes(remoteUrl: string): Promise<any> {
        return new Promise<any>((resolve, reject) => {
            cc.assetManager.loadRemote(remoteUrl, (error, data) => {
                if (error) {
                    cc.warn(error);
                    reject(error);
                } else {
                    resolve(data);
                }
            })
        })
    }

    /**
     * 加载文件夹资源Promise化
     * @param path 文件夹资源相对resources的路径
     * @param AssetsType 资源类型
     * @returns 
     */
    public static loadDir(path: string, AssetsType: any): Promise<any> {
        return new Promise<any>((resolve, reject) => {
            cc.resources.loadDir(path, AssetsType, (error, data) => {
                if (error) {
                    cc.warn(error);
                    reject(error);
                } else {
                    resolve(data);
                }
            })
        })
    }

    /**
     * 异步promise并发
     * @param arg 需要并发处理的函数数组
     * @returns 
     */
    public static concurrencyPromise(arg: Array<(resolve: Function, reject: Function) => void>): Promise<void> {
        return new Promise<void>((res, rej) => {
            let proArr: Promise<void>[] = []
            arg.forEach((cb: Function) => {
                proArr.push(new Promise<void>((resolve, reject) => {
                    cb(resolve, reject)
                }))
            })
            Promise.all(proArr).then(() => {
                res()
            }).catch((error) => {
                cc.warn(error)
                rej(error)
            })
        })
    }

    /**
     * 等待
     * @param time 时长（秒）
     * @returns dt两帧的间隔时间
     */
    public static sleep(time: number): Promise<number> {
        return new Promise(res => cc.Canvas.instance.scheduleOnce((dt: number) => { res(dt) }, time));
    }

    /**按钮点击缩放效果 */
    public static addBtnClickEffct(node: cc.Node, duration: number = 0.1): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            let preScale = node.scale;
            let pressScale = preScale - 0.1;
            cc.tween(node).to(duration, { scale: pressScale }).to(duration, { scale: preScale }).call(() => resolve()).start();
        })
    }

    /**放大缩小效果 */
    public static scaleEffct(node: cc.Node, duration: number = 0.3, scale: number = 0.1): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            let preScale = node.scale;
            let endScale = preScale + scale;
            cc.tween(node).to(duration, { scale: endScale }).to(duration, { scale: preScale }).call(() => resolve()).start();
        })
    }

    /**
     * 抖动效果 
     * @param target 节点
     * @param rotateAngles 旋转大小
     * @param rotateSeconds 旋转时间
     * @param rotateRepeatNum 重复次数
     * @returns 
     */
    public static shake(target: cc.Node, rotateAngles: number = 8, rotateSeconds: number = 0.05, rotateRepeatNum: number = 3): Promise<void> {
        cc.Tween.stopAllByTarget(target);
        return new Promise((resolve, reject) => {
            cc.tween(target)
                .set({ angle: 0 })
                .repeat(rotateRepeatNum, cc.tween()
                    .to(rotateSeconds, { angle: rotateAngles })
                    .to(rotateSeconds * 2, { angle: -rotateAngles })
                    .to(rotateSeconds, { angle: 0 })
                ).call(() => {
                    target.angle = 0;
                    resolve();
                }).start();
        });
    }

    /**上下一直摆动的效果 */
    public static upDownMove(node: cc.Node, duration: number = 0.3, Delta: number = 5): void {
        cc.tween(node).repeatForever(
            cc.tween().
                to(duration, { y: node.y + Utils.getRandomInt(1, Delta) }).
                to(duration * 0.9, { y: node.y }).
                to(duration, { y: node.y - Utils.getRandomInt(1, Delta) }).
                to(duration * 0.9, { y: node.y })
        ).start()
    }

    /**
     * 旋转需要放在update中 
     * @param node 节点
     * @param dt 两帧之差
     * @param angleNum 每帧旋转值
     * @param isCanRotate 当前是否可以旋转
     * @returns 
     */
    /**用来控制将节点angle置为原态 */
    static flag = false;
    public static rotateNode(node: cc.Node, dt: number, isCanRotate: boolean = false, angleNum: number = 20) {
        if (isCanRotate) {
            try {
                if (node) {
                    node.angle += angleNum * (1 - dt);
                    this.flag = true;
                } else {
                    cc.warn("node is null");
                    return
                }
            } catch (error) {
                console.log(error);
            }
        } else {
            if (this.flag) {
                node.angle = 0;
                this.flag = false;
            }
        }
    }


    /**
     * 16进制颜色转rgba颜色对象
     * @param ox 16进制颜色
     */
    public static oxToRgba(ox: string): cc.Color {
        return cc.color().fromHEX(ox)
    }

    /**
    * 将 16 进制（hex）颜色字符串转为 RGBA 格式
    * @param {string} hex 
    * @example
    * hexToRgba('#FFFFFF'); // { r: 255, g: 255, b: 255, a: 255 }
    */
    public static hexToRgba(hex: string) {
        if (!this.isHex(hex)) {
            return null;
        }
        const r = parseInt(hex.substring(1, 3), 16) || 0,
            g = parseInt(hex.substring(3, 5), 16) || 0,
            b = parseInt(hex.substring(5, 7), 16) || 0,
            a = parseInt(hex.substring(7), 16) || 255;
        return { r, g, b, a };
    }

    /**
     * 测试字符串是否为 16 进制（hex）颜色值
     * @param hex 
     */
    public static isHex(hex: string) {
        return /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(hex);
    }

    /**
     * 播放指定的龙骨动画
     * @param node 带有龙骨动画的节点
     * @param animation 要播放的动画名字 默认值是因为设计喜欢取这个动画名字
     * @param isSetNode 是否播放完后将节点隐藏
     * @param playNum 播放次数 -1为使用配置文件中的次数 0为无限循环播放
     * @param callbacks 每播放一次调用的回调函数
     * @returns 
     */
    public static playDragonBones(node: cc.Node, animation: string = "newAnimation", isSetNode: boolean = true, playNum: number = 1, callbacks?: Function): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            let num = 0;
            if (isSetNode) node.active = true;
            let dragonBonesComponent = node.getComponent(dragonBones.ArmatureDisplay);
            dragonBonesComponent.playAnimation(animation, playNum);
            dragonBonesComponent.on(dragonBones.EventObject.COMPLETE, function COMPLETE(event: any) {
                if (event.animationState.name == animation) {
                    callbacks && callbacks()
                    num++
                    if (num >= playNum) {
                        if (isSetNode) {
                            node.active = false;
                        }
                        dragonBonesComponent.off(dragonBones.EventObject.COMPLETE, COMPLETE)
                        resolve();
                    }
                }
            })
        })
    }

    /**
     * 返回龙骨动画的时长(自己根据定时器计算)
     * @param node 包含龙骨动画的节点
     * @param animation 需要统计的动画名称
     * @returns 
     */
    public static getDragonBonesTime(node: cc.Node, animation: string = "newAnimation"): Promise<number> {
        return new Promise<number>((resolve, reject) => {
            let timer: number = 0;
            let callbacks = (dt: number) => {
                timer += dt;
            }
            cc.Canvas.instance.schedule(callbacks, 0);
            let orginActive: boolean = node.active;
            node.active = true;
            node.opacity = 0;
            let dragonBonesComponent = node.getComponent(dragonBones.ArmatureDisplay);
            dragonBonesComponent.playAnimation(animation, 1);
            dragonBonesComponent.once(dragonBones.EventObject.COMPLETE, (event: any) => {
                if (event.animationState.name == animation) {
                    node.active = orginActive;
                    node.opacity = 255;
                    cc.Canvas.instance.unschedule(callbacks)
                    resolve(timer);
                }
            })
        })
    }

    /**
     * 返回龙骨动画的时长(通过私有属性需要当前龙骨动画的节点active为true)
     * @param node 包含龙骨动画的节点
     * @param animation 需要统计的动画名称
     * @returns 
     */
    public static getDBTime(node: cc.Node, animation: string = "newAnimation"): number {
        let orginActive = node.active;
        node.active = true
        let timer = node.getComponent(dragonBones.ArmatureDisplay).armature().animation.animations[animation].duration
        node.active = orginActive
        return timer
    }

    /**
    * 获取一个 min 到 max 范围内的随机整数
    * @param min 最小值
    * @param max 最大值
    * @param ranBurden 是否随机为正负数
    */
    public static getRandomInt(min: number = 0, max: number = 1, ranBurden: boolean = false): number {
        let flag = ranBurden ? Math.random() > 0.5 ? -1 : 1 : 1
        return Math.floor(Math.random() * (max - min) + min) * flag;
    }

    /**
     * 从一个点移动到另一个点
     * @param v1 开始点
     * @param v2 目的地
     * @param seed 速度
     * @returns 返回变换后的点
     */
    public static fromV1toV2(v1: cc.Vec2, v2: cc.Vec2, seed: number): cc.Vec2 {
        /**向量方向 */
        let dir = v2.sub(v1).normalize()
        v1.addSelf(dir.mul(seed))
        return v1
    }

    /** 
     * 将一个节点（0,0）位置转为世界坐标
    */
    public static v2ToWord(node: cc.Node) {
        return node.convertToWorldSpaceAR(cc.v2(0, 0))
    }

    /**
    * 将角度转为弧度
    * @param angle 角度
    */
    public static angleToRadian(angle: number): number {
        return angle * Math.PI / 180;
    }

    /**
    * 将弧度转为角度
    * @param radian 弧度
    */
    public static radianToAngle(radian: number): number {
        return radian * 180 / Math.PI;
    }

    /**
     * 返回一张带纯色精灵节点 
     * @param width 节点宽度
     * @param height 节点高度
     * @param color 节点颜色
     * @returns 
     */
    public static getSpriteSplashNode(width: number = 2, height: number = 2, color?: cc.Color): cc.Node {
        let node = new cc.Node();
        node.width = width;
        node.height = height;
        node.color = color;
        let tex = new cc.Texture2D();
        tex.initWithData(new Uint8Array(2 * 2 * 4).fill(255), cc.Texture2D.PixelFormat.RGBA8888, 2, 2);
        let sp = node.addComponent(cc.Sprite);
        sp.sizeMode = cc.Sprite.SizeMode.CUSTOM;
        sp.spriteFrame = new cc.SpriteFrame(tex)
        return node
    }

    /**
    * 节流 控制执行频率
    * @param timer 间隔时间(秒)
    * @returns boolean 是否间隔时间已到可以往下执行
    */
    public static throttle(timer: number = 0.5): boolean {
        if (Utils.throttle.prototype.flag) {
            return false
        } else {
            Utils.throttle.prototype.flag = true
            cc.Canvas.instance.scheduleOnce(() => {
                Utils.throttle.prototype.flag = null
            }, timer)
            return true
        }
    }

    /**
     * tween.to方法的简单promise封装
     * @param node 节点
     * @param duration 持续时间同tween.to
     * @param props 参数同tween.to
     * @param opts 参数同tween.to
     * @returns 
     */
    public static tweenTo(node: cc.Node, duration: number, props: ConstructorType<cc.Node>, opts?: Partial<{ progress: Function, easing: Function | String, onUpdate: Function }>, setProps?: ConstructorType<cc.Node>): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            try {
                cc.tween(node).set(setProps).to(duration, props, opts).call(() => {
                    resolve()
                }).start()
            } catch (error) {
                console.log(error);
                reject(error)
            }
        })
    }

    /**
     * 获取节点在目标节点（容器）下的相对位置 (好像有点问题)
     * @param node 节点
     * @param container 目标节点（容器）
     */
    public static getRelativePosition(node: cc.Node, container: cc.Node): cc.Vec2 {
        const worldPos = (node.getParent() || node).convertToWorldSpaceAR(node.getPosition());
        return container.convertToNodeSpaceAR(worldPos);
    }

    /**
     * 获取节点在目标节点（容器）下的相对位置 (没问题)
     * @param node 节点
     * @param target 目标节点（容器）
     */
    public static getNodeRelativePos(node: cc.Node, target: cc.Node): cc.Vec2 {
        const worldPos = (target.getParent() || target).convertToWorldSpaceAR(target.getPosition());
        return (node.getParent() || node).convertToNodeSpaceAR(worldPos);
    }

    /**
     * 深度拷贝(不能处理函数、循环引用等特殊情况)
     * @param obj 需要拷贝的对象
     * @returns 
     */
    public static deepCopy<T>(obj: T): T {
        if (typeof obj !== 'object' || obj === null) {
            return obj;
        }
        const result: any = Array.isArray(obj) ? [] : {};
        for (const key in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, key)) {
                const value = obj[key];
                result[key] = Utils.deepCopy(value);
            }
        }
        return result;
    }

    /**
     * 将对象 A 中有的属性 B 对象中没有的属性深度拷贝到 B 对象(注意对象深度太大可能会栈溢出)
     * @param a 对象A
     * @param b 对象B
     * @returns 
     */
    public static copyMissingProperties(a: Object, b: Object): Object {
        for (const key in a) {
            if (Object.prototype.hasOwnProperty.call(a, key)) {
                const value = a[key];
                if (!(key in b)) {
                    b[key] = Utils.deepCopy(value);
                } else if (typeof value === 'object' && value !== null) {
                    b[key] = Utils.copyMissingProperties(value, b[key]);
                }
            }
        }
        return b;
    }

    /**
     * 深度拷贝(迭代方式。不能处理函数、循环引用等特殊情况)
     * @param obj 需要拷贝的对象
     * @returns 
     */
    public static deepCopy2<T>(obj: T): T {
        const stack: [original: unknown, copy: unknown][] = [[obj, undefined]];
        const copied = new Map<unknown, unknown>();

        while (stack.length > 0) {
            const [original, copy] = stack.pop()!;
            if (typeof original !== 'object' || original === null) {
                copied.set(original, original);
                continue;
            }

            if (copied.has(original)) {
                continue;
            }

            const result: any = Array.isArray(original) ? [] : {};
            copied.set(original, result);

            for (const key in original) {
                if (Object.prototype.hasOwnProperty.call(original, key)) {
                    const value = original[key];
                    if (typeof value === 'object' && value !== null) {
                        stack.push([value, undefined]);
                    }
                    result[key] = copied.has(value) ? (copied.get(value) as any) : undefined;
                }
            }

            if (copy !== undefined) {
                (copy as any) = result;
            }
        }

        return copied.get(obj) as T;
    }

    /**
     * 将对象 A 中有的属性 B 对象中没有的属性深度拷贝到 B 对象迭代版本(对象深度太大用这个)
     * @param a 对象A
     * @param b 对象B
     * @returns 
     */
    public static copyMissingProperties2(a: Object, b: Object): Object {
        const copiedA = Utils.deepCopy2(a);
        return Object.assign({}, b, copiedA);
    }

    /**
     * 洗牌算法 不会改变原数组
     * @param array 需要打乱的数组
     * @returns 返回打乱后的新数组
     */
    public static shuffleArray<T>(array: T[]): T[] {
        const shuffledArray = [...array];
        for (let i = shuffledArray.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
        }
        return shuffledArray;
    }
}
